{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Unsupervised anomaly detection\n",
    "After our initial EDA, we have decided to pursue some unsupervised anomaly detection with a feature for the number of usernames with a failed login attempt in a given minute.\n",
    "\n",
    "## Setup"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>source_ip</th>\n",
       "      <th>username</th>\n",
       "      <th>success</th>\n",
       "      <th>failure_reason</th>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>datetime</th>\n",
       "      <th></th>\n",
       "      <th></th>\n",
       "      <th></th>\n",
       "      <th></th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>2018-01-01 00:06:19.353126</th>\n",
       "      <td>223.178.55.3</td>\n",
       "      <td>djones</td>\n",
       "      <td>1</td>\n",
       "      <td>None</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2018-01-01 00:09:07.147971</th>\n",
       "      <td>223.178.55.3</td>\n",
       "      <td>djones</td>\n",
       "      <td>1</td>\n",
       "      <td>None</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2018-01-01 01:08:08.610041</th>\n",
       "      <td>6.252.142.27</td>\n",
       "      <td>asmith</td>\n",
       "      <td>1</td>\n",
       "      <td>None</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2018-01-01 02:37:50.329298</th>\n",
       "      <td>124.178.25.98</td>\n",
       "      <td>akim</td>\n",
       "      <td>1</td>\n",
       "      <td>None</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2018-01-01 02:45:20.382080</th>\n",
       "      <td>98.43.141.103</td>\n",
       "      <td>akim</td>\n",
       "      <td>1</td>\n",
       "      <td>None</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "                                source_ip username  success failure_reason\n",
       "datetime                                                                  \n",
       "2018-01-01 00:06:19.353126   223.178.55.3   djones        1           None\n",
       "2018-01-01 00:09:07.147971   223.178.55.3   djones        1           None\n",
       "2018-01-01 01:08:08.610041   6.252.142.27   asmith        1           None\n",
       "2018-01-01 02:37:50.329298  124.178.25.98     akim        1           None\n",
       "2018-01-01 02:45:20.382080  98.43.141.103     akim        1           None"
      ]
     },
     "execution_count": 1,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "%matplotlib inline\n",
    "import matplotlib.pyplot as plt\n",
    "import numpy as np\n",
    "import pandas as pd\n",
    "import seaborn as sns\n",
    "import sqlite3\n",
    "\n",
    "with sqlite3.connect('logs/logs.db') as conn:\n",
    "    logs_2018 = pd.read_sql(\n",
    "        'SELECT * FROM logs WHERE datetime BETWEEN \"2018-01-01\" AND \"2019-01-01\";', \n",
    "        conn, parse_dates=['datetime'], index_col='datetime'\n",
    "    )\n",
    "logs_2018.head()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Prepping our data\n",
    "We need a function to transform our log data into our X for the model:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [],
   "source": [
    "def get_X(log, day):\n",
    "    \"\"\"\n",
    "    Get data we can use for the X\n",
    "    \n",
    "    Parameters:\n",
    "        - log: The logs dataframe\n",
    "        - day: A day or single value we can use as a datetime index slice\n",
    "    \n",
    "    Returns: \n",
    "        A pandas DataFrame\n",
    "    \"\"\"\n",
    "    return pd.get_dummies(log[day].assign(\n",
    "        failures=lambda x: 1 - x.success\n",
    "    ).query('failures > 0').resample('1min').agg(\n",
    "        {'username':'nunique', 'failures': 'sum'}\n",
    "    ).dropna().rename(\n",
    "        columns={'username':'usernames_with_failures'}\n",
    "    ).assign(\n",
    "        day_of_week=lambda x: x.index.dayofweek, \n",
    "        hour=lambda x: x.index.hour\n",
    "    ).drop(columns=['failures']), columns=['day_of_week', 'hour'])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We will work with January 2018:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "Index(['usernames_with_failures', 'day_of_week_0', 'day_of_week_1',\n",
       "       'day_of_week_2', 'day_of_week_3', 'day_of_week_4', 'day_of_week_5',\n",
       "       'day_of_week_6', 'hour_0', 'hour_1', 'hour_2', 'hour_3', 'hour_4',\n",
       "       'hour_5', 'hour_6', 'hour_7', 'hour_8', 'hour_9', 'hour_10', 'hour_11',\n",
       "       'hour_12', 'hour_13', 'hour_14', 'hour_15', 'hour_16', 'hour_17',\n",
       "       'hour_18', 'hour_19', 'hour_20', 'hour_21', 'hour_22', 'hour_23'],\n",
       "      dtype='object')"
      ]
     },
     "execution_count": 3,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "X = get_X(logs_2018, '2018-01')\n",
    "X.columns"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Isolation Forest\n",
    "with estimated 5% contamination:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "c:\\users\\molinstefanie\\packt\\venv\\lib\\site-packages\\sklearn\\preprocessing\\data.py:645: DataConversionWarning: Data with input dtype uint8, int64 were all converted to float64 by StandardScaler.\n",
      "  return self.partial_fit(X, y)\n",
      "c:\\users\\molinstefanie\\packt\\venv\\lib\\site-packages\\sklearn\\base.py:464: DataConversionWarning: Data with input dtype uint8, int64 were all converted to float64 by StandardScaler.\n",
      "  return self.fit(X, **fit_params).transform(X)\n"
     ]
    }
   ],
   "source": [
    "from sklearn.ensemble import IsolationForest\n",
    "from sklearn.pipeline import Pipeline\n",
    "from sklearn.preprocessing import StandardScaler\n",
    "\n",
    "iso_forest_pipeline = Pipeline([\n",
    "    ('scale', StandardScaler()),\n",
    "    ('iforest', IsolationForest(\n",
    "        random_state=0, contamination=0.05, behaviour='new'\n",
    "    ))\n",
    "]).fit(X)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Let's see how many outliers versus inliers we got. Outliers will be marked as -1:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "c:\\users\\molinstefanie\\packt\\venv\\lib\\site-packages\\sklearn\\pipeline.py:331: DataConversionWarning: Data with input dtype uint8, int64 were all converted to float64 by StandardScaler.\n",
      "  Xt = transform.transform(Xt)\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "inlier     39624\n",
       "outlier     1993\n",
       "dtype: int64"
      ]
     },
     "execution_count": 5,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "isolation_forest_preds = iso_forest_pipeline.predict(X)\n",
    "pd.Series(np.where(\n",
    "    isolation_forest_preds == -1, 'outlier', 'inlier'\n",
    ")).value_counts()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Local Outlier Factor\n",
    "Since we have no labeled data, we can't use grid search to tune our hyperparameters (we can't calculate performance metrics). Therefore, we will accept the default parameters for LOF, which will use 20 neighbors:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "c:\\users\\molinstefanie\\packt\\venv\\lib\\site-packages\\sklearn\\preprocessing\\data.py:645: DataConversionWarning: Data with input dtype uint8, int64 were all converted to float64 by StandardScaler.\n",
      "  return self.partial_fit(X, y)\n",
      "c:\\users\\molinstefanie\\packt\\venv\\lib\\site-packages\\sklearn\\base.py:464: DataConversionWarning: Data with input dtype uint8, int64 were all converted to float64 by StandardScaler.\n",
      "  return self.fit(X, **fit_params).transform(X)\n",
      "c:\\users\\molinstefanie\\packt\\venv\\lib\\site-packages\\sklearn\\neighbors\\lof.py:236: FutureWarning: default contamination parameter 0.1 will change in version 0.22 to \"auto\". This will change the predict method behavior.\n",
      "  FutureWarning)\n"
     ]
    }
   ],
   "source": [
    "from sklearn.neighbors import LocalOutlierFactor\n",
    "from sklearn.pipeline import Pipeline\n",
    "from sklearn.preprocessing import StandardScaler\n",
    "\n",
    "lof_pipeline = Pipeline([\n",
    "    ('scale', StandardScaler()),\n",
    "    ('lof', LocalOutlierFactor())\n",
    "]).fit(X)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "This model comes up with a negative outlier factor, which doesn't tell us outlier/inlier on its own:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "array([-1.91486343e+10, -1.00000000e+00, -1.00000000e+00, ...,\n",
       "       -1.00000000e+00, -1.00000000e+00, -1.91486343e+10])"
      ]
     },
     "execution_count": 7,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "lof_preds = lof_pipeline.named_steps['lof'].negative_outlier_factor_ \n",
    "lof_preds"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "For that, we need to compare it to the offset. Values less than the offset are outliers:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "inlier     41577\n",
       "outlier       40\n",
       "dtype: int64"
      ]
     },
     "execution_count": 8,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "pd.Series(np.where(\n",
    "    lof_preds < lof_pipeline.named_steps['lof'].offset_, 'outlier', 'inlier'\n",
    ")).value_counts()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Check agreement between unsupervised methods\n",
    "While we can't compare their performance without labeled data, we can see if they are generally in agreement (they aren't):"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "0.012896350639585386"
      ]
     },
     "execution_count": 9,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "from sklearn.metrics import cohen_kappa_score\n",
    "\n",
    "is_lof_outlier = np.where(\n",
    "    lof_preds < lof_pipeline.named_steps['lof'].offset_, \n",
    "    'outlier', 'inlier'\n",
    ")\n",
    "is_iso_outlier = np.where(\n",
    "    isolation_forest_preds == -1, 'outlier', 'inlier'\n",
    ")\n",
    "\n",
    "cohen_kappa_score(is_lof_outlier, is_iso_outlier)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Evaluating the models\n",
    "We have been given the labeled data. Now we can truly compare these models."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "(111, 6)"
      ]
     },
     "execution_count": 10,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "with sqlite3.connect('logs/logs.db') as conn:\n",
    "    hackers_2018 = pd.read_sql(\n",
    "        \"\"\"\n",
    "        SELECT * \n",
    "        FROM attacks \n",
    "        WHERE start BETWEEN \"2018-01-01\" AND \"2019-01-01\";\n",
    "        \"\"\", conn, parse_dates=['start', 'end']\n",
    "    ).assign(\n",
    "        duration=lambda x: x.end - x.start,\n",
    "        start_floor=lambda x: x.start.dt.floor('min'),\n",
    "        end_ceil=lambda x: x.end.dt.ceil('min')\n",
    "    )\n",
    "hackers_2018.shape"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Note this only has an IP address for one of the IP addresses involved in each attack, so it's a good thing we aren't relying on that anymore. Also note that, while the attacks are quick in duration, our minutely data means we will trigger many alerts per attack:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>start</th>\n",
       "      <th>end</th>\n",
       "      <th>source_ip</th>\n",
       "      <th>duration</th>\n",
       "      <th>start_floor</th>\n",
       "      <th>end_ceil</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>2018-01-05 06:03:42.470259</td>\n",
       "      <td>2018-01-05 06:03:51.470259</td>\n",
       "      <td>170.9.4.108</td>\n",
       "      <td>00:00:09</td>\n",
       "      <td>2018-01-05 06:03:00</td>\n",
       "      <td>2018-01-05 06:04:00</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>2018-01-11 03:08:43.284085</td>\n",
       "      <td>2018-01-11 03:09:14.284085</td>\n",
       "      <td>27.255.30.3</td>\n",
       "      <td>00:00:31</td>\n",
       "      <td>2018-01-11 03:08:00</td>\n",
       "      <td>2018-01-11 03:10:00</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>2018-01-17 00:41:43.985324</td>\n",
       "      <td>2018-01-17 00:45:56.985324</td>\n",
       "      <td>226.98.192.152</td>\n",
       "      <td>00:04:13</td>\n",
       "      <td>2018-01-17 00:41:00</td>\n",
       "      <td>2018-01-17 00:46:00</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>2018-01-21 10:34:57.842776</td>\n",
       "      <td>2018-01-21 10:38:01.842776</td>\n",
       "      <td>102.178.107.171</td>\n",
       "      <td>00:03:04</td>\n",
       "      <td>2018-01-21 10:34:00</td>\n",
       "      <td>2018-01-21 10:39:00</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>2018-01-21 23:12:10.852725</td>\n",
       "      <td>2018-01-21 23:12:38.852725</td>\n",
       "      <td>48.172.61.152</td>\n",
       "      <td>00:00:28</td>\n",
       "      <td>2018-01-21 23:12:00</td>\n",
       "      <td>2018-01-21 23:13:00</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "                       start                        end        source_ip  \\\n",
       "0 2018-01-05 06:03:42.470259 2018-01-05 06:03:51.470259      170.9.4.108   \n",
       "1 2018-01-11 03:08:43.284085 2018-01-11 03:09:14.284085      27.255.30.3   \n",
       "2 2018-01-17 00:41:43.985324 2018-01-17 00:45:56.985324   226.98.192.152   \n",
       "3 2018-01-21 10:34:57.842776 2018-01-21 10:38:01.842776  102.178.107.171   \n",
       "4 2018-01-21 23:12:10.852725 2018-01-21 23:12:38.852725    48.172.61.152   \n",
       "\n",
       "  duration         start_floor            end_ceil  \n",
       "0 00:00:09 2018-01-05 06:03:00 2018-01-05 06:04:00  \n",
       "1 00:00:31 2018-01-11 03:08:00 2018-01-11 03:10:00  \n",
       "2 00:04:13 2018-01-17 00:41:00 2018-01-17 00:46:00  \n",
       "3 00:03:04 2018-01-21 10:34:00 2018-01-21 10:39:00  \n",
       "4 00:00:28 2018-01-21 23:12:00 2018-01-21 23:13:00  "
      ]
     },
     "execution_count": 11,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "hackers_2018.head()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We want to mark each minute that had an attack, so we can use the `start_floor` and `end_ceil` columns to create a range of datetimes. Then, we can check if the data we marked as outliers falls within that range:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {},
   "outputs": [],
   "source": [
    "def get_y(datetimes, hackers, resolution='1min'):\n",
    "    \"\"\"\n",
    "    Get data we can use for the y (whether or not a hacker attempted a log in during that time).\n",
    "    \n",
    "    Parameters:\n",
    "        - datetimes: The datetimes to check for hackers\n",
    "        - hackers: The dataframe indicating when the attacks started and stopped\n",
    "        - resolution: The granularity of the datetime. Default is 1 minute.\n",
    "        \n",
    "    Returns:\n",
    "        A pandas Series of booleans.\n",
    "    \"\"\"\n",
    "    date_ranges = hackers.apply(\n",
    "        lambda x: pd.date_range(x.start_floor, x.end_ceil, freq=resolution), \n",
    "        axis=1\n",
    "    )\n",
    "    dates = pd.Series()\n",
    "    for date_range in date_ranges:\n",
    "        dates = pd.concat([dates, date_range.to_series()])\n",
    "    return datetimes.isin(dates)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Let's grab our labeled `y` data:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {},
   "outputs": [],
   "source": [
    "is_hacker = get_y(X.reset_index().datetime, hackers_2018)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We will create partials for the performance metrics functions for less typing:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {},
   "outputs": [],
   "source": [
    "from functools import partial\n",
    "from sklearn.metrics import classification_report\n",
    "from ml_utils.classification import confusion_matrix_visual\n",
    "\n",
    "report = partial(classification_report, is_hacker)\n",
    "conf_matrix = partial(\n",
    "    confusion_matrix_visual, is_hacker, class_labels=[False, True]\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Isolation forest"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "              precision    recall  f1-score   support\n",
      "\n",
      "       False       1.00      0.95      0.98     41588\n",
      "        True       0.00      0.31      0.01        29\n",
      "\n",
      "   micro avg       0.95      0.95      0.95     41617\n",
      "   macro avg       0.50      0.63      0.49     41617\n",
      "weighted avg       1.00      0.95      0.97     41617\n",
      "\n"
     ]
    }
   ],
   "source": [
    "iso_forest_predicts_hacker = isolation_forest_preds == - 1\n",
    "\n",
    "print(report(iso_forest_predicts_hacker))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Local Outlier Factor"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "              precision    recall  f1-score   support\n",
      "\n",
      "       False       1.00      1.00      1.00     41588\n",
      "        True       0.55      0.76      0.64        29\n",
      "\n",
      "   micro avg       1.00      1.00      1.00     41617\n",
      "   macro avg       0.77      0.88      0.82     41617\n",
      "weighted avg       1.00      1.00      1.00     41617\n",
      "\n"
     ]
    }
   ],
   "source": [
    "lof_predicts_hacker = lof_preds < lof_pipeline.named_steps['lof'].offset_\n",
    "\n",
    "print(report(lof_predicts_hacker))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Comparing confusion matrices"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "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\n",
      "text/plain": [
       "<Figure size 1080x360 with 4 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    }
   ],
   "source": [
    "fig, axes = plt.subplots(1, 2, figsize=(15, 5))\n",
    "conf_matrix(iso_forest_predicts_hacker, ax=axes[0], title='Isolation Forest')\n",
    "conf_matrix(lof_predicts_hacker, ax=axes[1], title='Local Outlier Factor')"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.7.2"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
